Coverage Report

Created: 2026-04-26 08:04

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
D:\a\csshw\csshw\xtask\src\coverage.rs
Line
Count
Source
1
//! Local coverage report generation.
2
//!
3
//! The nightly toolchain is required because `#[coverage(off)]` — used to
4
//! exclude untestable code such as Windows API wrappers and production I/O
5
//! implementations from coverage — relies on the `coverage_attribute` feature,
6
//! which is only available on nightly Rust. Without it the `cfg(coverage_nightly)`
7
//! guards would not activate, causing those impls to be counted as missed lines
8
//! and distorting the report.
9
//!
10
//! The pinned toolchain version is read from
11
//! `.config/coverage/nightly-toolchain.version` and the filename exclusion
12
//! regex is read from `.config/coverage/ignore-filename.regex`; both files
13
//! are shared with the CI workflow to keep the environments in sync.
14
//!
15
//! [`run_coverage`] orchestrates the full workflow: toolchain check,
16
//! instrumented test run, and report generation.
17
18
use anyhow::{bail, Context, Result};
19
20
/// All side-effecting operations required by this module.
21
///
22
/// Implement with mocks in tests to achieve zero filesystem, process,
23
/// and toolchain side-effects.
24
pub trait CoverageSystem {
25
    /// Read the contents of `.config/coverage/nightly-toolchain.version` and
26
    /// return the trimmed toolchain identifier (e.g. `nightly-2026-04-20`).
27
    ///
28
    /// # Errors
29
    ///
30
    /// Returns an error if the file cannot be read.
31
    fn read_nightly_version_file(&self) -> Result<String>;
32
33
    /// Run `rustup toolchain list` and return its stdout.
34
    ///
35
    /// # Errors
36
    ///
37
    /// Returns an error if the process cannot be started.
38
    fn list_installed_toolchains(&self) -> Result<String>;
39
40
    /// Run `rustup toolchain install <toolchain> --component llvm-tools`.
41
    ///
42
    /// # Errors
43
    ///
44
    /// Returns an error if the install fails.
45
    fn install_toolchain(&self, toolchain: &str) -> Result<()>;
46
47
    /// Run a `cargo +<toolchain> llvm-cov` subcommand with the given arguments.
48
    ///
49
    /// # Errors
50
    ///
51
    /// Returns an error if the command fails.
52
    fn run_cargo_llvm_cov(&self, toolchain: &str, args: &[String]) -> Result<()>;
53
54
    /// Read the contents of `.config/coverage/ignore-filename.regex` and
55
    /// return the trimmed filename regex pattern passed to
56
    /// `--ignore-filename-regex`.
57
    ///
58
    /// # Errors
59
    ///
60
    /// Returns an error if the file cannot be read.
61
    fn read_ignore_regex_file(&self) -> Result<String>;
62
63
    /// Print an informational message to stdout.
64
    fn print_info(&self, message: &str);
65
}
66
67
/// Production implementation of [`CoverageSystem`].
68
pub struct RealSystem;
69
70
#[cfg_attr(coverage_nightly, coverage(off))]
71
impl CoverageSystem for RealSystem {
72
    fn read_nightly_version_file(&self) -> Result<String> {
73
        std::fs::read_to_string(".config/coverage/nightly-toolchain.version")
74
            .context("failed to read .config/coverage/nightly-toolchain.version")
75
            .map(|s| s.trim().to_owned())
76
    }
77
78
    fn list_installed_toolchains(&self) -> Result<String> {
79
        let output = std::process::Command::new("rustup")
80
            .args(["toolchain", "list"])
81
            .output()
82
            .context("failed to run `rustup toolchain list`")?;
83
        Ok(String::from_utf8_lossy(&output.stdout).into_owned())
84
    }
85
86
    fn install_toolchain(&self, toolchain: &str) -> Result<()> {
87
        let status = std::process::Command::new("rustup")
88
            .args([
89
                "toolchain",
90
                "install",
91
                toolchain,
92
                "--component",
93
                "llvm-tools",
94
            ])
95
            .status()
96
            .context("failed to run `rustup toolchain install`")?;
97
        if !status.success() {
98
            bail!("`rustup toolchain install {toolchain}` failed with status {status}");
99
        }
100
        Ok(())
101
    }
102
103
    fn run_cargo_llvm_cov(&self, toolchain: &str, args: &[String]) -> Result<()> {
104
        let toolchain_arg = format!("+{toolchain}");
105
        let status = std::process::Command::new("cargo")
106
            .arg(&toolchain_arg)
107
            .arg("llvm-cov")
108
            .args(args)
109
            .status()
110
            .with_context(|| {
111
                format!(
112
                    "failed to run `cargo {toolchain_arg} llvm-cov {}`",
113
                    args.join(" ")
114
                )
115
            })?;
116
        if !status.success() {
117
            bail!(
118
                "`cargo {toolchain_arg} llvm-cov {}` failed with status {status}",
119
                args.join(" ")
120
            );
121
        }
122
        Ok(())
123
    }
124
125
    fn read_ignore_regex_file(&self) -> Result<String> {
126
        std::fs::read_to_string(".config/coverage/ignore-filename.regex")
127
            .context("failed to read .config/coverage/ignore-filename.regex")
128
            .map(|s| s.trim().to_owned())
129
    }
130
131
    fn print_info(&self, message: &str) {
132
        println!("INFO - {message}");
133
    }
134
}
135
136
/// Convert a slice of string literals to a `Vec<String>`.
137
13
fn args(values: &[&str]) -> Vec<String> {
138
59
    
values13
.
iter13
().
map13
(|s| (*s).to_owned()).
collect13
()
139
13
}
140
141
/// Generate coverage reports using the pinned nightly toolchain.
142
///
143
/// Reads the toolchain identifier from
144
/// `.config/coverage/nightly-toolchain.version`, ensures it is installed,
145
/// cleans stale coverage data, runs the test suite with instrumentation,
146
/// and produces Cobertura XML and HTML reports.
147
///
148
/// # Arguments
149
///
150
/// * `system` - Injected I/O provider.
151
///
152
/// # Errors
153
///
154
/// Returns an error if any step fails (missing version file, toolchain
155
/// install failure, test failure, or report generation failure).
156
6
pub fn run_coverage<S: CoverageSystem>(system: &S) -> Result<()> {
157
6
    let 
toolchain5
= system.read_nightly_version_file()
?1
;
158
5
    system.print_info(&format!("Using nightly toolchain: {toolchain}"));
159
5
    let ignore_regex = system.read_ignore_regex_file()
?0
;
160
161
    // Ensure toolchain is installed.
162
5
    let installed = system.list_installed_toolchains()
?0
;
163
5
    if installed.lines().any(|line| line.starts_with(&toolchain)) {
164
3
        system.print_info("Toolchain already installed");
165
3
    } else {
166
2
        system.print_info(&format!("Installing toolchain: {toolchain}"));
167
2
        system.install_toolchain(&toolchain)
?1
;
168
    }
169
170
    // Clean previous coverage data.
171
4
    system.print_info("Cleaning previous coverage data");
172
4
    system.run_cargo_llvm_cov(&toolchain, &args(&["clean", "--workspace"]))
?1
;
173
174
    // Run tests with coverage instrumentation.
175
3
    system.print_info("Running tests with coverage");
176
3
    system.run_cargo_llvm_cov(
177
3
        &toolchain,
178
3
        &args(&[
179
3
            "--all-features",
180
3
            "--workspace",
181
3
            "--no-report",
182
3
            "--",
183
3
            "--no-capture",
184
3
        ]),
185
0
    )?;
186
187
    // Generate Cobertura XML report.
188
3
    system.print_info("Generating Cobertura XML report");
189
3
    system.run_cargo_llvm_cov(
190
3
        &toolchain,
191
3
        &args(&[
192
3
            "report",
193
3
            "--cobertura",
194
3
            "--output-path",
195
3
            "coverage.xml",
196
3
            "--ignore-filename-regex",
197
3
            &ignore_regex,
198
3
        ]),
199
0
    )?;
200
201
    // Generate HTML report.
202
3
    system.print_info("Generating HTML report");
203
3
    system.run_cargo_llvm_cov(
204
3
        &toolchain,
205
3
        &args(&[
206
3
            "report",
207
3
            "--html",
208
3
            "--output-dir",
209
3
            "coverage_html",
210
3
            "--ignore-filename-regex",
211
3
            &ignore_regex,
212
3
        ]),
213
0
    )?;
214
215
3
    system.print_info("Coverage reports generated:");
216
3
    system.print_info("  XML:  coverage.xml");
217
3
    system.print_info("  HTML: coverage_html/index.html");
218
3
    Ok(())
219
6
}
220
221
#[cfg(test)]
222
#[path = "tests/test_coverage.rs"]
223
mod tests;